﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Web.Razor;
using System.Web.Razor.Generator;
using Microsoft.CSharp;
using PropertyExpression.Common;
using System.Data;
using Roslyn.Compilers;
using Roslyn.Compilers.Common;
using Roslyn.Compilers.CSharp;

namespace PropertyExpression.ControllableQuery
{
    //TODO: сделать internal
    public static class Util
    {
        //TODO: сделать internal
        public static object ParamInference<T>(T arg)
        {
            return Cache<T>.Func(arg);
        }

        private static class Cache<T>
        {
            internal static readonly Func<T, object> Func = GetFunc();

            private static Func<T, object> GetFunc()
            {
                var type = typeof(T);
                if (DynamicMaterializer.IsAnonymousType(type))
                    return DynamicMaterializer.BuildAnonymousTypeParamInference<T>();
                //TODO: добавить остальные типы
                //TODO: добавить Func<TResult>. 2014-01-11 12:34 Func<TResult> не совсем подходит лучше сделаать 
                //вывод параметров для PropertyExpression.ControllableQuery.Lazy,
                //фактически Param.New(() => ...) является краткой записью для Lazy.New(() => ....Param())
                if (type == typeof(int)) return _ => ((int) (object) _).Param();
                if (type == typeof(string)) return _ => ((string)(object)_).Param();
                if (type == typeof(Guid)) return _ => ((Guid)(object)_).Param();
                if (type == typeof(DateTime)) return _ => ((DateTime)(object)_).Param();
                if (type == typeof(decimal)) return _ => ((decimal)(object)_).Param();
                if (type.IsGenericType)
                {
                    var genericTypeDefinition = type.GetGenericTypeDefinition();
                    if (genericTypeDefinition == typeof (Option<>))
                    {
                        var argument = type.GetGenericArguments()[0];
                        return (Func<T, object>) selectMethod.MakeGenericMethod(argument, ParamInferenceType(argument))
                            .CreateDelegate(typeof (Func<T, object>));
                    }
                    if (genericTypeDefinition == typeof(Nullable<>))
                    {
                        var argument = type.GetGenericArguments()[0];
                        return (Func<T, object>)nullableSelectMethod.MakeGenericMethod(argument, ParamInferenceType(argument))
                            .CreateDelegate(typeof(Func<T, object>));
                    }
                    if (genericTypeDefinition == typeof(Func<>))
                    {
                        var argument = type.GetGenericArguments()[0];
                        return (Func<T, object>)funcMethod.MakeGenericMethod(argument, ParamInferenceType(argument))
                            .CreateDelegate(typeof(Func<T, object>));
                    }
                }
                return _ => _;
            }
        }

        private static readonly MethodInfo funcMethod =
            QWriter.GetMethodInfo<Func<Func<object>, object>>(x1 => SelectForFunc<object, object>(x1))
                .GetGenericMethodDefinition();

        private static object SelectForFunc<T, TResult>(Func<T> arg)
        {
            return Func.Lazy(() => (TResult) Cache<T>.Func(arg()));
        }

        private static readonly MethodInfo nullableSelectMethod =
            QWriter.GetMethodInfo<Func<int?, object>>(x1 => Select<int, int>(x1))
                .GetGenericMethodDefinition();

        private static object Select<T, TResult>(T? arg) where T: struct where TResult: struct
        {
            return arg.HasValue ? (TResult) Cache<T>.Func(arg.Value) : new TResult?();
        }

        private static readonly MethodInfo selectMethod =
            QWriter.GetMethodInfo<Func<Option<object>, object>>(x1 => Select<object, object>(x1))
                .GetGenericMethodDefinition();

        private static object Select<T, TResult>(Option<T> arg)
        {
            return arg.Select(_ => (TResult) Cache<T>.Func(_));
        }

        public static Type ParamInferenceType(Type type)
        {
            if (DynamicMaterializer.IsAnonymousType(type))
            {
                if (!type.IsGenericType) return type;
                return ParamInferenceTypeForAnonymousType(type);
            }
            //TODO: добавить остальные типы
            if (type == typeof (int)) return typeof (Param<int>);
            if (type == typeof(string)) return typeof(Param<string>);
            if (type == typeof(Guid)) return typeof(Param<Guid>);
            if (type == typeof(DateTime)) return typeof(Param<DateTime>);
            if (type == typeof(decimal)) return typeof(Param<decimal>);
            if (type.IsGenericType)
            {
                var genericTypeDefinition = type.GetGenericTypeDefinition();
                if (genericTypeDefinition == typeof (Option<>))
                    return typeof (Option<>).MakeGenericType(ParamInferenceType(type.GetGenericArguments()[0]));
                if (genericTypeDefinition == typeof(Nullable<>))
                    return typeof(Nullable<>).MakeGenericType(ParamInferenceType(type.GetGenericArguments()[0]));
                if (genericTypeDefinition == typeof(Func<>))
                    return typeof(Func<>).MakeGenericType(ParamInferenceType(type.GetGenericArguments()[0]));
            }
            return type;
        }

        internal static Type ParamInferenceTypeForAnonymousType(Type type)
        {
            return type.GetGenericTypeDefinition()
                .MakeGenericType(type.GetGenericArguments().Select(ParamInferenceType).ToArray());
        }

        internal static TResult ParamInferenceInternal<T, TResult>(T arg)
        {
            return (TResult) Cache<T>.Func(arg);
        }

        internal static readonly MethodInfo ParamInferenceMethod =
            QWriter.GetMethodInfo<Func<object, object>>(x1 => ParamInferenceInternal<object, object>(x1))
                .GetGenericMethodDefinition();

        public static string GetQueryPath(string filePath, int lineNumber)
        {
            var info = new FileInfo(filePath);
            return Path.Combine(info.DirectoryName,
                Path.GetFileNameWithoutExtension(info.Name) + "_" + lineNumber + ".cshtml");
        }

        public const string WriteToMethodName = "PropertyExpression.ControllableQuery.QWriter.WriteTo";
        public const string WriteLiteralToMethodName = "PropertyExpression.ControllableQuery.QWriter.WriteLiteralTo";

        private static readonly RazorCache razorCache = new RazorCache();

        private class RazorCache
        {
            private readonly ConcurrentDictionary<int, Item> dictionary = new ConcurrentDictionary<int, Item>();

            public Option<RazorCacheItem> Get(int lineNumber, string filePath)
            {
                Item value;
                return dictionary.TryGetValue(lineNumber, out value) ? value.Get(filePath) : new Option<RazorCacheItem>();
            }

            public void Set(int lineNumber, string filePath, RazorCacheItem item)
            {
                Item value;
                if (dictionary.TryGetValue(lineNumber, out value))
                    value.Set(lineNumber, filePath, item);
                else
                    dictionary[lineNumber] = new SingleItem(item, filePath, this);
            }

            private abstract class Item
            {
                public abstract Option<RazorCacheItem> Get(string aFilePath);
                public abstract void Set(int lineNumber, string aFilePath, RazorCacheItem aItem);
            }

            private class SingleItem : Item
            {
                private readonly RazorCacheItem item;
                private readonly string filePath;
                private readonly RazorCache parent;

                public SingleItem(RazorCacheItem item, string filePath, RazorCache parent)
                {
                    this.item = item;
                    this.filePath = filePath;
                    this.parent = parent;
                }

                public override Option<RazorCacheItem> Get(string aFilePath)
                {
                    return filePath == aFilePath ? item : new Option<RazorCacheItem>();
                }

                public override void Set(int lineNumber, string aFilePath, RazorCacheItem aItem)
                {
                    parent.dictionary[lineNumber] =
                        new MultipleItem(new ConcurrentDictionary<string, RazorCacheItem>(new[] {
                        new KeyValuePair<string, RazorCacheItem>(filePath, item),
                        new KeyValuePair<string, RazorCacheItem>(aFilePath, aItem)
                    }));
                }
            }

            private class MultipleItem : Item
            {
                private readonly ConcurrentDictionary<string, RazorCacheItem> items;

                public MultipleItem(ConcurrentDictionary<string, RazorCacheItem> items)
                {
                    this.items = items;
                }

                public override Option<RazorCacheItem> Get(string aFilePath)
                {
                    RazorCacheItem value;
                    return items.TryGetValue(aFilePath, out value) ? value : new Option<RazorCacheItem>();
                }

                public override void Set(int lineNumber, string aFilePath, RazorCacheItem aItem)
                {
                    items[aFilePath] = aItem;
                }
            }
        }

        private class RazorCacheItem
        {
            public readonly Func<object, QWriter, TemplateBase> Func;
            public readonly string Sql;
            public readonly Type ParamType;

            public RazorCacheItem(Func<object, QWriter, TemplateBase> func, string sql, Type paramType)
            {
                Func = func;
                Sql = sql;
                ParamType = paramType;
            }
        }

        internal static IQuery<Type<T>> Query<T, TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath)
        {
            return GetQBuilder(param, sql, commandType, lineNumber, filePath).Query<T>();
        }

        internal static IQuery<T> BatchQuery<T, TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath)
        {
            return GetQBuilder(param, sql, commandType, lineNumber, filePath).BatchQuery<T>();
        }

        internal static INonQuery NonQuery<TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath)
        {
            return GetQBuilder(param, sql, commandType, lineNumber, filePath).NonQuery();
        }

        internal static QWriter GetQBuilder<TParam>(TParam param, string sql, CommandType commandType,
            int lineNumber, string filePath)
        {
            var builder = new QWriter(commandType);
            var paramInference = ParamInference(param);
                QFragment(paramInference, sql, lineNumber, filePath)(builder);
            if (commandType == CommandType.StoredProcedure)
                builder.AddParams((dynamic) paramInference);
            return builder;
        }

        public static QFragment QFragment(this object p, string text, [CallerLineNumber] int lineNumber = 0,
            [CallerFilePath] string filePath = "")
        {
            var value = razorCache.Get(lineNumber, filePath);
            var type = p.GetType();
            Func<object, QWriter, TemplateBase> @delegate;
            if (value.HasValue)
            {
                var razorCacheItem = value.Value;
                if (razorCacheItem.Sql != text) throw new ApplicationException(); //TODO: написать сообщение
                if (razorCacheItem.ParamType != type) throw new ApplicationException(); //TODO: написать сообщение
                @delegate = razorCacheItem.Func;
            }
            else
            {
                @delegate = BuildQFragment(type, text, lineNumber, filePath);
                razorCache.Set(lineNumber, filePath, new RazorCacheItem(@delegate, text, type));
            }
            return builder => @delegate(p, builder).Execute();
        }

        public static Func<object, QWriter, TemplateBase> BuildQFragment(Type type, string text,
            [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "")
        {
            //TODO: теперь имя класса может быть постоянным, поскольку эмитем в отдельную сборку. Сделать это.
            var template = "T" + Guid.NewGuid().ToString("N");
            var host = new RazorEngineHost(new CSharpRazorCodeLanguage()) {
                GeneratedClassContext = new GeneratedClassContext(
                    executeMethodName: GeneratedClassContext.Default.ExecuteMethodName,
                    writeMethodName: GeneratedClassContext.Default.WriteMethodName,
                    writeLiteralMethodName: GeneratedClassContext.Default.WriteLiteralMethodName,
                    writeToMethodName: WriteToMethodName,
                    writeLiteralToMethodName: WriteLiteralToMethodName,
                    templateTypeName: typeof (QFragment).FullName,
                    defineSectionMethodName: GeneratedClassContext.Default.DefineSectionMethodName),
                DefaultBaseClass = typeof (TemplateBase).FullName,
                DefaultClassName = template
            };
            var razorConfiguration = RazorConfigurationHelper.Get(filePath, lineNumber);
            foreach (var namespaceImport in razorConfiguration.Item1)
                host.NamespaceImports.Add(namespaceImport);

            var engine = new RazorTemplateEngine(host);
            GeneratorResults razorResult;
            using (var reader = new StringReader(text))
                razorResult = engine.GenerateCode(reader, className: null, rootNamespace: null,
                    sourceFileName: GetQueryPath(filePath, lineNumber));
            if (!razorResult.Success)
            {
                var error = razorResult.ParserErrors[0];
                throw new ApplicationException(
                    String.Format("Razor parsing error: '{3}' (Caller line number {4}, Ln {0}, Col {1}) {2}",
                        error.Location.LineIndex + 1, error.Location.CharacterIndex + 1, error.Message,
                        filePath, lineNumber));
            }
            StringWriter stringWriter;
            using (stringWriter = new StringWriter())
            using (var codeProvider = new CSharpCodeProvider())
                codeProvider.GenerateCodeFromCompileUnit(
                    razorResult.GeneratedCode, stringWriter, new CodeGeneratorOptions());

            var propertyInfos = type.GetProperties().Select(_ => new {
                _.Name,
                _.PropertyType,
                SetterName = "." + _.Name
            }).ToList();
            if (propertyInfos.All(_ => _.Name != "p"))
                propertyInfos.Add(new {
                    Name = "p",
                    PropertyType = type,
                    SetterName = ""
                });

            var types = new Dictionary<Assembly, string>();
            var syntax = GetTypeSyntax(type, types);
            var typeSyntaxs = propertyInfos.Select(_ => GetTypeSyntax(_.PropertyType, types)).ToList();

            var compilationUnitSyntax = SyntaxTree.ParseText(stringWriter + @"
namespace Razor
{
" + String.Join(@"
", types.Select(_ => "    extern alias " + _.Value + ";")) + @"

    public partial class " + template + @"
    {
        internal static " + typeof (TemplateBase).FullName + @" M58a3d6d024dc47c6a692e20e29210b89(object arg, " +
                typeof (QWriter).FullName + @" writer) {
            var arg2 = (System.Object)arg;
            var result = new " + template + @"(writer);
" + String.Join(@"
", propertyInfos.Select(_ => "            result." + _.Name + " = arg2" + _.SetterName + ";")) + @"
            return result;
        }

        public " + template + @"(" + typeof (QWriter).FullName + @" builder): base(builder) {}

" + String.Join(@"
", propertyInfos.Select(_ => "        internal System.Object " + _.Name + ";")) + @"
    }
}
").GetRoot();
            var descendantNodes = compilationUnitSyntax.ChildNodes().Skip(1).First().DescendantNodes();
            var syntaxTree = SyntaxTree.Create((CompilationUnitSyntax) new Rewriter(
                descendantNodes.OfType<MethodDeclarationSyntax>().First().DescendantNodes()
                    .OfType<CastExpressionSyntax>().First(),
                syntax,
                descendantNodes.OfType<FieldDeclarationSyntax>().Select(
                    _ => _.DescendantNodes().OfType<VariableDeclarationSyntax>().Single())
                    .Take(typeSyntaxs.Count).Select((declarationSyntax, i) => new {
                        declarationSyntax,
                        typeSyntax = typeSyntaxs[i]
                    }).ToDictionary(_ => _.declarationSyntax, _ => _.typeSyntax)
                ).Visit(compilationUnitSyntax));
            var compilation = Compilation.Create(DynamicMaterializer.AssemblyName.Name,
                new CompilationOptions(OutputKind.DynamicallyLinkedLibrary),
                new[] {syntaxTree},
                types.Select(_ => new MetadataFileReference(_.Key.Location,
                    new MetadataReferenceProperties(alias: _.Value)))
                    .Concat(razorConfiguration.Item2
                        .Select(_ => new MetadataFileReference(_.Location))));
            var className = "Razor." + template;
            CommonEmitResult emitResult;
            Func<Type> typeFunc;
            if (!Debugger.IsAttached)
            {
                emitResult = compilation.Emit(DynamicMaterializer.ModuleBuilder);
                typeFunc = () => DynamicMaterializer.ModuleBuilder.GetType(className);
            }
            else
            {
                MemoryStream stream;
                MemoryStream pdbStream;
                using (stream = new MemoryStream())
                using (pdbStream = new MemoryStream())
                    emitResult = compilation.Emit(stream, pdbStream: pdbStream);
                typeFunc = () => Assembly.Load(stream.GetBuffer(), pdbStream.GetBuffer()).GetType(className);
            }

            foreach (var diagnostic in emitResult.Diagnostics)
            {
                var lineSpan = diagnostic.Location.GetLineSpan(usePreprocessorDirectives: true);
                throw new ApplicationException(
                    string.Format("Build error: '{3}' (Caller line number {4}, Ln {0}, Col {1}) {2}",
                        lineSpan.StartLinePosition.Line + 1,
                        lineSpan.StartLinePosition.Character + 1,
                        diagnostic.Info.GetMessage(),
                        filePath, lineNumber));
            }

            return (Func<object, QWriter, TemplateBase>) typeFunc()
                .GetMethod("M58a3d6d024dc47c6a692e20e29210b89", BindingFlags.Static | BindingFlags.NonPublic)
                .CreateDelegate(typeof (Func<object, QWriter, TemplateBase>));
        }

        private class RazorEngineHost : System.Web.Razor.RazorEngineHost
        {
            public RazorEngineHost(RazorCodeLanguage codeLanguage)
                : base(codeLanguage)
            {
            }

            public override void PostProcessGeneratedCode(CodeGeneratorContext context)
            {
                base.PostProcessGeneratedCode(context);
                context.GeneratedClass.IsPartial = true;
                context.GeneratedClass.Members.Remove(
                    context.GeneratedClass.Members.OfType<CodeConstructor>().Single());
            }
        }

        private class Rewriter : SyntaxRewriter
        {
            private readonly CastExpressionSyntax castExpressionSyntax;
            private readonly TypeSyntax syntax;
            private readonly Dictionary<VariableDeclarationSyntax, TypeSyntax> dictionary;

            public Rewriter(CastExpressionSyntax castExpressionSyntax, TypeSyntax syntax,
                Dictionary<VariableDeclarationSyntax, TypeSyntax> dictionary)
            {
                this.castExpressionSyntax = castExpressionSyntax;
                this.syntax = syntax;
                this.dictionary = dictionary;
            }

            public override SyntaxNode VisitVariableDeclaration(VariableDeclarationSyntax node)
            {
                TypeSyntax value;
                return dictionary.TryGetValue(node, out value)
                    ? node.WithType(value)
                    : base.VisitVariableDeclaration(node);
            }

            public override SyntaxNode VisitCastExpression(CastExpressionSyntax node)
            {
                return node == castExpressionSyntax
                    ? castExpressionSyntax.WithType(syntax)
                    : base.VisitCastExpression(node);
            }
        }

        private static TypeSyntax GetTypeSyntax(Type type, Dictionary<Assembly, string> dictionary)
        {
            if (type.Namespace != null)
                return NameSyntax(type, dictionary);
            else if (DynamicMaterializer.IsAnonymousType(type))
            {
                string value;
                if (!dictionary.TryGetValue(type.Assembly, out value))
                {
                    value = "r" + dictionary.Count;
                    dictionary.Add(type.Assembly, value);
                }
                return Syntax.AliasQualifiedName(value, SimpleNameSyntax(type, dictionary));
            }
            else
                return SimpleNameSyntax(type, dictionary);
        }

        private static NameSyntax NameSyntax(Type type, Dictionary<Assembly, string> dictionary)
        {
            return Syntax.QualifiedName(
                type.IsNested
                    ? NameSyntax(type.DeclaringType, dictionary)
                    : Syntax.ParseName(type.Namespace),
                SimpleNameSyntax(type, dictionary));
        }

        private static SimpleNameSyntax SimpleNameSyntax(Type type, Dictionary<Assembly, string> dictionary)
        {
            SimpleNameSyntax syntax;
            if (!type.IsGenericType)
                syntax = Syntax.IdentifierName(type.Name);
            else
                syntax = Syntax.GenericName(
                    Syntax.Identifier(type.Name.Substring(0, type.Name.IndexOf("`", StringComparison.Ordinal))),
                    Syntax.TypeArgumentList(
                        Syntax.SeparatedList<TypeSyntax>(
                            WithComma(type.GetGenericArguments().Select(_ => GetTypeSyntax(_, dictionary))).ToArray()
                            )
                        )
                    );
            return syntax;
        }

        private static IEnumerable<SyntaxNodeOrToken> WithComma(IEnumerable<TypeSyntax> enumerable)
        {
            var isFirst = true;
            foreach (var typeSyntax in enumerable)
            {
                if (!isFirst)
                    yield return Syntax.Token(SyntaxKind.CommaToken);
                else
                    isFirst = false;
                yield return typeSyntax;
            }
        }

        public const BindingFlags AllBindingFlags = BindingFlags.Default |
            BindingFlags.IgnoreCase |
            BindingFlags.DeclaredOnly |
            BindingFlags.Instance |
            BindingFlags.Static |
            BindingFlags.Public |
            BindingFlags.NonPublic |
            BindingFlags.FlattenHierarchy |
            BindingFlags.InvokeMethod |
            BindingFlags.CreateInstance |
            BindingFlags.GetField |
            BindingFlags.SetField |
            BindingFlags.GetProperty |
            BindingFlags.SetProperty |
            BindingFlags.PutDispProperty |
            BindingFlags.PutRefDispProperty |
            BindingFlags.ExactBinding |
            BindingFlags.SuppressChangeType |
            BindingFlags.OptionalParamBinding |
            BindingFlags.IgnoreReturn;
    }

    public delegate void QFragment(QWriter writer);

    public class QWriter
    {
        internal readonly CommandType CommandType;
        internal readonly StringBuilder Builder = new StringBuilder();
        private readonly List<Func<SqlParameter>> sqlParameters = new List<Func<SqlParameter>>();
        internal readonly Dictionary<object, string> Params = new Dictionary<object, string>();
        private readonly Dictionary<object, Action<SqlParameter>> paramActions = new Dictionary<object, Action<SqlParameter>>();

        private void AddParam<T>(string name, Param<T> param)
        {
            sqlParameters.Add(() => {
                var sqlParameter = param.Content.SqlParameterFunc(name);
                Action<SqlParameter> action;
                if (paramActions.TryGetValue(param, out action)) action(sqlParameter);
                if (param.Content.SetValue.HasValue) param.Content.SetValue.Value(sqlParameter);
                return sqlParameter;
            });
            Params.Add(param, name);
        }

        internal static readonly MethodInfo AddParamMethod =
            GetMethodInfo<Action<QWriter, string, Param<object>>>((x1, x2, x3) => x1.AddParam(x2, x3))
                .GetGenericMethodDefinition();

        public QWriter(CommandType commandType = CommandType.Text)
        {
            CommandType = commandType;
        }

        public QWriter WriteLiteral(string arg)
        {
            Builder.Append(arg);
            return this;
        }

        public QWriter Write<T>(Param<T> param)
        {
            string name;
            if (!Params.TryGetValue(param, out name))
            {
                name = "@" + sqlParameters.Count;
                AddParam(name, param);
            }
            return WriteLiteral(name);
        }

        public void ForParam<T>(Param<T> param, Action<SqlParameter> action)
        {
            paramActions.Add(param, action);
        }

        public QWriter AddParams<T>(T param)
        {
            Cache<T>.Action(this, param);
            return this;
        }

        public QWriter Write(QFragment arg)
        {
            arg(this);
            return this;
        }

        public QWriter Write<T>(Param<T>? arg)
        {
            return Write(arg.Value);
        }

        public QWriter Write<T>(Option<Param<T>> arg)
        {
            return Write(arg.Value);
        }

        private static class Cache<T>
        {
            public static readonly Action<QWriter, T> Action = DynamicMaterializer.BuildParamAdder<T>();
        }

        public static void WriteTo<T>(QWriter writer, Param<T> arg)
        {
            writer.Write(arg);
        }

        public static void WriteTo<T>(QWriter writer, Param<T>? arg)
        {
            writer.Write(arg);
        }

        public static void WriteTo<T>(QWriter writer, Option<Param<T>> arg)
        {
            writer.Write(arg);
        }

        public static void WriteTo(QWriter writer, QFragment arg)
        {
            writer.Write(arg);
        }

        public static void WriteLiteralTo(QWriter writer, string arg)
        {
            writer.WriteLiteral(arg);
        }

        public static MethodInfo GetMethodInfo<T>(Expression<T> expression)
        {
            return ((MethodCallExpression) expression.Body).Method;
        }

        public static void ForParamTo<T>(QWriter writer, Param<T> param, Action<SqlParameter> action)
        {
            writer.ForParam(param, action);
    }

        public void AddParameters(SqlCommand command)
        {
            sqlParameters.ForEach(func => command.Parameters.Add(func()));
        }
    }

    public abstract class TemplateBase
    {
        private readonly QWriter writer;

        protected TemplateBase(QWriter writer)
        {
            this.writer = writer;
        }

        public abstract void Execute();

        protected void Write<T>(Param<T> arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void Write<T>(Param<T>? arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void Write<T>(Option<Param<T>> arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void Write(QFragment arg)
        {
            QWriter.WriteTo(writer, arg);
        }

        protected void WriteLiteral(string arg)
        {
            QWriter.WriteLiteralTo(writer, arg);
        }

        protected void ForParam<T>(Param<T> param, Action<SqlParameter> action)
        {
            QWriter.ForParamTo(writer, param, action);
        }
    }

    public interface IRazorConfiguration
    {
        Tuple<IEnumerable<string>, IEnumerable<Assembly>> Get(MethodBase method, string file, int line,
            Tuple<IEnumerable<string>, IEnumerable<Assembly>> current);
    }

    public class RazorConfigurationHelper
    {
        private static IEnumerable<Assembly> assemblies;
        private static string connectionString;
        private static string tableName;

        public static void SetConfiguratio(IEnumerable<Assembly> _assemblies, string _connectionString, string _tableName)
        {
            assemblies = _assemblies;
            connectionString = _connectionString;
            tableName = _tableName;
        }

        public static Tuple<IEnumerable<string>, IEnumerable<Assembly>> Get(string filePath, int lineNumber)
        {
            MethodBase method;
            bool tryGetValue;
            lock (sourceLocationToMethodMap)
                tryGetValue = sourceLocationToMethodMap.TryGetValue(Tuple.Create(filePath, lineNumber), out method);
            if (!tryGetValue)
                lock (sourceLocationToMethodMap)
                {
                    List<string> typeNames;
                    using (var connection = new SqlConnection(connectionString))
                    {
                        connection.Open();
                        using (var command = connection.CreateCommand())
                        {
                            //TODO: использовать CQ
                            command.CommandText = "SELECT TypeName FROM " + tableName;
                            typeNames = SelectTypeNames(command).ToList();
                        }
                    }
                    var deserializeTypes = DeserializeTypes(typeNames);
                    if (!deserializeTypes.HasValue)
                        return ProcessAllAndGet(filePath, lineNumber);
                    else
                    {
                        Process(deserializeTypes.Value);
                        if (sourceLocationToMethodMap.TryGetValue(Tuple.Create(filePath, lineNumber), out method))
                            return GetByMethod(filePath, lineNumber, method);
                        else
                            return ProcessAllAndGet(filePath, lineNumber);
                    }
                }
            else
                return GetByMethod(filePath, lineNumber, method);
        }

        private static Tuple<IEnumerable<string>, IEnumerable<Assembly>> ProcessAllAndGet(string filePath,
            int lineNumber)
        {
            MethodBase method;
            Process(assemblies.SelectMany(_ => _.GetTypes()));
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = "DELETE FROM " + tableName;
                    command.ExecuteNonQuery();
                }
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = "INSERT INTO " + tableName + " (Id, TypeName) VALUES (@Id, @TypeName)";
                    var id = command.Parameters.Add(new SqlParameter("@Id", SqlDbType.UniqueIdentifier));
                    var typeName = command.Parameters.Add(new SqlParameter("@TypeName", SqlDbType.NVarChar, -1));
                    foreach (var type in sourceLocationToMethodMap.Values.Select(_ => _.DeclaringType).Distinct())
                    {
                        id.Value = Guid.NewGuid();
                        typeName.Value = type.AssemblyQualifiedName;
                        command.ExecuteNonQuery();
                    }
                }
            }
            if (sourceLocationToMethodMap.TryGetValue(Tuple.Create(filePath, lineNumber), out method))
                return GetByMethod(filePath, lineNumber, method);
            else
                throw new ApplicationException();
        }

        private static Tuple<IEnumerable<string>, IEnumerable<Assembly>> GetByMethod(string filePath, int lineNumber,
            MethodBase method)
        {
            var assembly = method.DeclaringType.Assembly;
            var razorConfiguration = ((IRazorConfiguration) assembly.CreateInstance(
                assembly.GetTypes().Single(_ => _.GetInterfaces().Contains(typeof (IRazorConfiguration))).FullName));
            return razorConfiguration.Get(method, filePath, lineNumber,
                Tuple.Create(GetItUsing(method),
                    method.DeclaringType.Assembly.GetReferencedAssemblies().Select(_ => Assembly.Load(_.FullName))
                        .Concat(new[] {method.DeclaringType.Assembly})));
        }

        private static void Process(IEnumerable<Type> types)
        {
            sourceLocationToMethodMap.Clear();
            foreach (var type in types)
            {
                foreach (var methodInfo in type.GetMethods(Util.AllBindingFlags))
                    Process(methodInfo, methodInfo.GetGenericArguments);
                foreach (var methodInfo in type.GetConstructors(Util.AllBindingFlags))
                    Process(methodInfo, () => new Type[] {});
            }
        }

        private static Option<List<Type>> DeserializeTypes(IEnumerable<string> typeNames)
        {
            var types = new List<Type>();
            foreach (var typeName in typeNames)
            {
                var type = Type.GetType(typeName);
                if (type == null) return new Option<List<Type>>();
                types.Add(type);
            }
            return types;
        }

        private static IEnumerable<string> SelectTypeNames(SqlCommand command)
        {
            using (var reader = command.ExecuteReader())
                while (reader.Read())
                    yield return reader.GetString(0);
        }

        private static IEnumerable<string> GetItUsing(MethodBase methodBase)
        {
            var declaringName = GetDeclaringName(methodBase);
            if (declaringName.HasValue)
                yield return "It = " + declaringName.Value;
        }

        private static Option<string> GetDeclaringName(MemberInfo memberInfo)
        {
            var declaringType = memberInfo.DeclaringType;
            if (declaringType == null) return new Option<string>();
            if (declaringType.IsGenericType) return new Option<string>();
            return declaringType.FullName;
        }

        private static readonly Dictionary<Tuple<string, int>, MethodBase> sourceLocationToMethodMap =
            new Dictionary<Tuple<string, int>, MethodBase>();

        private static void Process(MethodBase methodInfo, Func<Type[]> genericMethodArgumentsFunc)
        {
            var methodBody = methodInfo.GetMethodBody();
            if (methodBody == null) return;
            var ilAsByteArray = methodBody.GetILAsByteArray();
            var module = methodInfo.Module;
            ILProcessor.ProcessMethodBody(
                ilAsByteArray,
                module,
                inlineMethod:
                    (metadataToken, position, ilInstructions) => {
                        var resolveMember = module.ResolveMember(
                            metadataToken,
                            methodInfo.DeclaringType.GetGenericArguments(),
                            genericMethodArgumentsFunc());
                        switch (resolveMember.MemberType)
                        {
                            case MemberTypes.Method:
                                var resolveMethodInfo = (MethodInfo) resolveMember;
                                if (ILProcessor.qFragmentMethodInfo.Equals(resolveMethodInfo)
                                    || ILProcessor.buildQFragmentMethodInfo.Equals(resolveMethodInfo)
                                    || (
                                    resolveMethodInfo.IsGenericMethod &&
                                        ILProcessor.NewNonQueryMethodInfo.Equals(
                                            resolveMethodInfo.GetGenericMethodDefinition())
                                    )
                                    || (
                                        resolveMethodInfo.DeclaringType != null &&
                                            resolveMethodInfo.DeclaringType.IsGenericType &&
                                            resolveMethodInfo.DeclaringType.GetGenericTypeDefinition() ==
                                                typeof (Query<>) &&
                                            resolveMethodInfo.Name == ILProcessor.NewQueryMethodInfo.Name
                                        )
                                    ||
                                    (
                                        resolveMethodInfo.DeclaringType != null &&
                                            resolveMethodInfo.DeclaringType.IsGenericType &&
                                            resolveMethodInfo.DeclaringType.GetGenericTypeDefinition() ==
                                                typeof (BatchQuery<>) &&
                                            resolveMethodInfo.Name == ILProcessor.newBatchQueryMethodInfo.Name
                                        ))
                                {
                                    var index = ilInstructions.Count - 1;
                                    if (index < 0) throw new ApplicationException(); //TODO: написать сообщение
                                    var filePath = ILProcessor.GetFilePath(ilInstructions[index]);
                                    index--;
                                    if (index < 0) throw new ApplicationException(); //TODO: написать сообщение
                                    var lineNumber = ILProcessor.GetLineNumber(ilInstructions[index]);
                                    sourceLocationToMethodMap.Add(Tuple.Create(filePath, lineNumber), methodInfo);
                                    return false;
                                }
                                break;
                        }
                        return false;
                    },
                inlineField: delegate { },
                inlineTok: delegate { },
                inlineType: delegate { });
        }
    }

    public static class NonQuery
    {
        public static INonQuery New<TParam>(TParam param, string sql, CommandType commandType = CommandType.Text,
            [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "")
        {
            return Util.NonQuery(param, sql, commandType, lineNumber, filePath);
        }        
    }

    public static class BatchQuery<T>
    {
        public static IQuery<T> New<TParam>(TParam param, string sql, CommandType commandType = CommandType.Text,
            [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "")
        {
            return Util.BatchQuery<T, TParam>(param, sql, commandType, lineNumber, filePath);
        }
    }

    public static class Query<T>
    {
        public static IQuery<Type<T>> New<TParam>(TParam param, string sql, CommandType commandType = CommandType.Text,
            [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "")
        {
            return Util.Query<T, TParam>(param, sql, commandType, lineNumber, filePath);
        }
    }

    public interface IQuery
    {
        string CommandText { get; }
        void AddParameters(SqlCommand command);
        CommandType CommandType { get; }
        string GetParamName<T>(Param<T> param);
    }

// ReSharper disable UnusedTypeParameter
    public interface IQuery<T> : IQuery
// ReSharper restore UnusedTypeParameter
    {
    }

    public interface INonQuery : IQuery
    {
    }

    /// <summary>
    /// //TODO: вытровить
    /// </summary>
    public class ColumnSelector
    {
        internal readonly List<string> ColumnRefs = new List<string>();

        public T _<T>()
        {
            ColumnRefs.Add(string.Empty);
            return default(T);
        }

        public T _<T>(string columnRef)
        {
            ColumnRefs.Add(columnRef);
            return default(T);
        }

        public string String()
        {
            return _<string>();
        }

        public int Int()
        {
            return _<int>();
        }

        public int Int(string columnRef)
        {
            return _<int>(columnRef);
        }

        public DateTime DateTime()
        {
            return _<DateTime>();
        }

    }

    public static class QueryExtensions
    {
        public static void SetQuery(this SqlCommand command, IQuery query)
        {
            command.CommandType = query.CommandType;
            command.CommandText = query.CommandText;
            query.AddParameters(command);
        }

        public static void SetValue<T>(this SqlCommand command, IQuery query, Param<T> param, T value)
        {
            command.Parameters[query.GetParamName(param)].Value = value;
        }

        public static Option<T> GetValue<T>(this SqlCommand command, IQuery query, Param<T> param)
        {
            var value = command.Parameters[query.GetParamName(param)].Value;
            return value == DBNull.Value ? new Option<T>() : (T) value;
        }

        public static Option<T> GetValue<T>(this SqlCommand command, IQuery query, Param<Option<T>> param)
        {
            var value = command.Parameters[query.GetParamName(param)].Value;
            return value == DBNull.Value ? new Option<T>() : (T) value;
        }

        private static class Cache<T>
        {
            public static readonly List<string> ConstructorParameterNames =
                typeof(T).GetConstructors()[0].GetParameters().Select(_ => _.Name).ToList();
        }

        public static IQuery<Type<T>> Select<T>(this QWriter it, Func<ColumnSelector, T> columnSelectorFunc)
        {
            if (!DynamicMaterializer.IsAnonymousType(typeof(T))) throw new NotSupportedException();

            var columnSelector = new ColumnSelector();
            columnSelectorFunc(columnSelector);

            var parameterNames = Cache<T>.ConstructorParameterNames;
            var stringBuilder = new StringBuilder();
            for (var index = 0; index < parameterNames.Count; index++)
            {
                if (stringBuilder.Length != 0) stringBuilder.Append(@",
");
                stringBuilder.Append("    " + columnSelector.ColumnRefs[index] + parameterNames[index]);
            }
            it.Builder.Insert(0, stringBuilder.Insert(0, @"SELECT
").AppendLine());
            return it.BatchQuery<Type<T>>();
        }

        public static IQuery<Type<T>> Query<T>(this QWriter it, T recordExample)
        {
            return it.BatchQuery<Type<T>>();
        }

        public static IQuery<Type<T>> Query<T>(this QWriter it)
        {
            return it.BatchQuery<Type<T>>();
        }

        /// <summary>
        /// //TODO: убрать. Соответствующего Razor аналога нет.
        /// </summary>
        public static IQuery<Type<T1, T2>> Query<T1, T2>(this QWriter it)
        {
            return it.BatchQuery<Type<T1, T2>>();
        }

        public static IQuery<T> BatchQuery<T>(this QWriter it)
        {
            return CreateQuery<T>(it);
        }

        public static Tuple<IQuery<T1>, IQuery<T2>> BatchQuery<T1, T2>(this Tuple<QWriter, QWriter> it)
        {
            return Tuple.Create(CreateQuery<T1>(it.Item1), CreateQuery<T2>(it.Item2));
        }

        private static IQuery<T> CreateQuery<T>(QWriter builder)
        {
            return new QueryImpl<T>(builder);
        }

        private class QueryImpl<T> : IQuery<T>
        {
            private readonly QWriter writer;
            private readonly string commandText;

            public QueryImpl(QWriter writer)
            {
                this.writer = writer;
                commandText = writer.Builder.ToString();
            }

            public string CommandText
            {
                get { return commandText; }
            }

            public void AddParameters(SqlCommand command)
            {
                writer.AddParameters(command);
            }

            public CommandType CommandType
            {
                get { return writer.CommandType; }
            }

            public string GetParamName<TParam>(Param<TParam> param)
            {
                return writer.Params[param];
            }
        }

        public static Type QueryImplementationType
        {
            get { return typeof (QueryImpl<>); }
        }

        public static INonQuery NonQuery(this QWriter builder)
        {
            return new NonQueryImpl(builder);
        }

        private class NonQueryImpl : INonQuery
        {
            private readonly QWriter writer;
            private readonly string commandText;

            public NonQueryImpl(QWriter writer)
            {
                this.writer = writer;
                commandText = writer.Builder.ToString();
            }

            public string CommandText
            {
                get { return commandText; }
            }

            public void AddParameters(SqlCommand command)
            {
                writer.AddParameters(command);
            }

            public CommandType CommandType
            {
                get { return writer.CommandType; }
            }

            public string GetParamName<T>(Param<T> param)
            {
                return writer.Params[param];
            }
        }

        public static Type NonQueryImplementationType
        {
            get { return typeof(NonQueryImpl); }
        }
    }
}